package com.yugao.fintech.antelope.auth.third;

import cn.hutool.core.util.RandomUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.github.yitter.idgen.YitIdHelper;
import com.yugao.fintech.antelope.auth.assembly.ThirdAuthAssembly;
import com.yugao.fintech.antelope.auth.constants.CacheKeyEnum;
import com.yugao.fintech.antelope.auth.controller.ThirdAuthController;
import com.yugao.fintech.antelope.auth.model.ThirdBindContent;
import com.yugao.fintech.antelope.auth.model.entity.ExtLdpConnect;
import com.yugao.fintech.antelope.auth.model.entity.LocalUser;
import com.yugao.fintech.antelope.auth.model.entity.SocialUser;
import com.yugao.fintech.antelope.auth.model.rqrs.AuthUserSaveReq;
import com.yugao.fintech.antelope.auth.model.rqrs.UserResp;
import com.yugao.fintech.antelope.auth.model.rqrs.oauth2.TokenContextBuildReq;
import com.yugao.fintech.antelope.auth.model.rqrs.third.*;
import com.yugao.fintech.antelope.auth.oauth2.OAuth2TokenContext;
import com.yugao.fintech.antelope.auth.oauth2.model.OAuth2TokenResp;
import com.yugao.fintech.antelope.auth.oauth2.model.RegisteredClient;
import com.yugao.fintech.antelope.auth.oauth2.support.AuthHandler;
import com.yugao.fintech.antelope.auth.service.*;
import com.yugao.fintech.antelope.base.config.core.ConfigFactory;
import com.yugao.fintech.antelope.infra.common.QrCodeSceneEnum;
import com.yugao.fintech.antelope.infra.common.ScanScene;
import com.yugao.fintech.antelope.base.model.constants.AppType;
import com.yugao.fintech.antelope.base.model.constants.RequestCons;
import com.yugao.fintech.antelope.base.model.constants.SexEnum;
import com.yugao.fintech.antelope.base.model.constants.ValidationCons;
import com.yugao.fintech.antelope.base.model.enums.AuthErrorEnum;
import com.yugao.fintech.antelope.base.model.module.auth.GrantTypeEnum;
import com.yugao.fintech.antelope.mps.api.MsgPushApi;
import com.yugao.fintech.antelope.mps.api.model.rqrs.CheckNoAuthUserCaptchaReq;
import com.yugao.fintech.framework.assistant.utils.RegexUtils;
import com.yugao.fintech.framework.assistant.utils.StringUtils;
import com.yugao.fintech.framework.assistant.utils.exception.Assert;
import com.yugao.fintech.framework.assistant.utils.exception.BizException;
import com.yugao.fintech.framework.core.utils.ServletUtils;
import com.yugao.fintech.framework.lock.constant.LockBeanName;
import com.yugao.fintech.framework.lock.manager.ILockManager;
import com.yugao.fintech.framework.lock.model.ILock;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

/**
 * 社交授权接口
 */
@Slf4j
public abstract class BaseThirdAuth {
    private final static Map<GrantTypeEnum, BaseThirdAuth> map = new ConcurrentHashMap<>();
    @Autowired
    protected ThirdAuthAssembly thirdAssembly;
    @Autowired
    protected ConfigFactory configFactory;
    @Autowired
    protected SocialUserService socialUserService;
    @Autowired
    protected OAuth2TokenService tokenService;
    @Autowired
    protected ExtLdpConnectService ldpConnectService;
    @Autowired
    protected UserService userService;
    @Autowired
    protected RedisTemplate<String, Object> redisTemplate;
    @Autowired
    protected MsgPushApi msgPushApi;
    @Autowired
    protected OAuth2ClientService clientService;
    @Autowired
    private LocalUserService localUserService;
    @Autowired
    protected CaptchaService captchaService;
    @Autowired
    protected AuthHandler authHandler;

    @Resource(name = LockBeanName.REDISSON_SERVICE)
    private ILockManager lockManager;

    @PostConstruct
    public void initBaseThirdAuth() {
        map.put(grantType(), this);
    }

    /**
     * 获取实例
     */
    public static BaseThirdAuth getInstance(GrantTypeEnum grantType) {
        if (Objects.isNull(grantType)) {
            throw new BizException("授权方式不能为空");
        }
        if (!map.containsKey(grantType)) {
            throw new BizException(AuthErrorEnum.NO_IMPL_GRANT_TYPE, grantType);
        }
        return map.get(grantType);
    }

    /**
     * 自动绑定用户
     *
     * @param authUser 授权用户
     */
    private void autoBindUser(RegisteredClient client, ThirdAuthUser authUser) {
        HttpServletRequest request = ServletUtils.getRequestOfNonNull();
        String terminal = request.getHeader(RequestCons.Headers.TERMINAL);

        AppType appType = client.getAppType();
        // 查找第三方用户
        SocialUser socialUser = socialUserService.getByOpenId(grantType(), appType, authUser.getOpenId());

        String lockKey = String.format("lock:auto_bind_user:%s:%s", authUser.getSource(), authUser.getOpenId());
        ILock lock = lockManager.getLock(lockKey);

        try {
            if (!lock.tryLock(5, TimeUnit.SECONDS)) {
                throw new BizException("请不要频繁进行登录操作");
            }
            SocialUser newSocialUser = SocialUser.builder().source(grantType().getCodeNumber())
                    .appType(client.getAppType().getCode())
                    .authId(Optional.ofNullable(socialUser).map(SocialUser::getAuthId).orElse(null))
                    .openId(authUser.getOpenId()).build();

            if (Objects.isNull(socialUser) || Objects.isNull(socialUser.getUserId())) {
                // 查找身份源
                ExtLdpConnect ldpConnect = ldpConnectService.getByGrantType(grantType());

                // 1. 非自动绑定用户
                if (Boolean.TRUE.equals(ldpConnect.getIsAskBind())) {
                    authUser.setIsBind(false);
                    authUser.setIsFirstBind(true);
                    // 自动创建第三方用户
                    if (Objects.isNull(socialUser)) {
                        socialUserService.save(newSocialUser);
                    }
                    return;
                }

                // 2. 自动绑定用户
                AuthUserSaveReq userSaveReq = new AuthUserSaveReq();
                userSaveReq.setUsername(StringUtils.defaultIfEmpty(authUser.getUsername(), YitIdHelper.nextId() + ""))
                        .setEmail(authUser.getEmail()).setPhone(authUser.getPhone()).setAvatar(authUser.getAvatar())
                        .setNickName(authUser.getNickname()).setSex(authUser.getSex()).setTerminal(terminal);

                Long userId = userService.saveUser(userSaveReq, client.getAppType());

                authUser.setIsBind(true);
                authUser.setIsFirstBind(true);
                authUser.setUserId(userId);
                newSocialUser.setUserId(userId);

                socialUserService.saveOrUpdate(newSocialUser);
                log.info("success bind third user: {}", JSON.toJSONString(newSocialUser));
            } else {
                LocalUser localUser = localUserService.getUserByUserId(socialUser.getUserId());
                authUser.setUsername(localUser.getUserName());
                authUser.setIsBind(true);
                authUser.setIsFirstBind(false);
                authUser.setUserId(socialUser.getUserId());
            }
        } finally {
            lock.unlock();
        }

    }

    public ThirdAuthResp authorize(AuthUrlGetReq req) {
        return doAuthorize(req);
    }

    /**
     * 第三方登录
     *
     * @return 返回登录成功后的用户信息
     */
    @Transactional(rollbackFor = Exception.class)
    public ThirdAuthUser login(ThirdLoginReq req) {
        // step: 执行第三方登录逻辑-主要获取用户信息
        ThirdAuthUser authUser = doLogin(req);
        if (Objects.isNull(authUser)) {
            throw new BizException("处理用户登录失败");
        }
        authUser.setSource(grantType().getCode());
        authUser.setLoginId(String.valueOf(YitIdHelper.nextId()));

        // step: 执行绑定用户逻辑
        autoBindUser(req.getClient(), authUser);

        // step: 办法token, 无论用户是否已经绑定平台用户都要颁发token, 假如用户没有绑定平台用户且没有开启自动绑定,
        // 那么前端要引导用户进行绑定, 在绑定已有账号或者创建新账号的时候都要携带该令牌, 即接口必须授权才能访问
        TokenContextBuildReq tokenContextBuildReq = TokenContextBuildReq.builder()
                .userId(authUser.getUserId()).loginId(authUser.getLoginId()).client(req.getClient())
                .tokenClaims(authUser.getTokenClaims()).loginAccount(authUser.getOpenId())
                .grantType(grantType()).openId(authUser.getOpenId()).build();

        OAuth2TokenContext tokenContext = tokenService.buildTokenContent(tokenContextBuildReq);
        OAuth2TokenResp token = tokenService.issueToken(tokenContext);

        if (authUser.getIsBind()) {
            authHandler.onSuccess(req.getClient(), tokenContext.getAuthUser());
        }
        authUser.setAuthToken(token);
        return authUser;
    }

    /**
     * 用户获取第三方的用户信息, 比如微信小程序可能会获取用户的手机号
     */
    public ThirdUserInfoResp getUserInfo(ThirdUserGetReq req) {
        throw new BizException("该渠道不支持主动获取用户信息");
    }

    /**
     * 绑定校验, 默认校验方式是 手机号/邮箱 + 验证码, 子类可以进行覆盖, 比如微信小程序无需手机号 + 验证码方式绑定, 而是
     * 采用微信提供的获取手机号方式来可信的获取手机号, 就可以覆盖当前方法的空实现,
     * 目的是不校验手机号 + 验证码, 然后再实现 {@link #fillBindUserInfo(ThirdBindContent)} 通过微信小程序颁发的
     * code获取手机号
     * <p>
     * 如果小程序端想要支持多种绑定方式, 比如默认使用微信小程序官网提供的获取手机号方式来绑定账号, 现在想要支持用户可以手动输入
     * 手机号 + 验证码方式进行绑定, 可以再定义一个grant_type, 但是一般情况下只需要微信提供的获取手机号进行绑定即可(目前该接口收费)
     */
    protected void bindVerify(ThirdBindContent content) {
        Assert.notEmpty(content.getVerifyCode(), "动态码" + ValidationCons.NOT_EMPTY);
        Assert.notEmpty(content.getAccount(), "账号" + ValidationCons.NOT_EMPTY);
        Assert.notEmpty(content.getTaskCode(), "任务编码" + ValidationCons.NOT_EMPTY);
        Assert.notEmpty(content.getImageCaptcha(), "验证码" + ValidationCons.NOT_EMPTY);
        Assert.notEmpty(content.getImageUuid(), "验证码标识" + ValidationCons.NOT_EMPTY);

        // 校验图形验证码
        captchaService.checkCaptcha(content.getImageCaptcha(), content.getImageUuid());

        // 检验邮箱/手机号验证
        CheckNoAuthUserCaptchaReq messageReq = new CheckNoAuthUserCaptchaReq();
        messageReq.setTaskCode(content.getTaskCode());
        messageReq.setVerifyCode(content.getVerifyCode());
        messageReq.setToUser(content.getAccount());
        msgPushApi.checkNoAuthUserCaptcha(messageReq);
    }

    /**
     * 填充绑定的用户信息, 账号必须填写, 子类可以覆盖
     */
    protected void fillBindUserInfo(ThirdBindContent content) {

    }

    /**
     * 第一次绑定用户信息, 绑定流程
     * 1. 判断第三方账号是否被其他账号绑定, 如果已经被绑定
     * 需要用户通过 手机号/邮箱 + 验证码 或者 用户名 + 密码 或者使用小程序快捷登录 登录原有账号进行解绑
     * 2. 判断绑定的本地账号是否存在, 如果不存在就创建一个账号(因为是通过 手机号/邮箱 + 验证码 或者小程序获取到手机号)是可信的, 所以这里
     * 可以直接创建账号
     * 3. 对帐号进行绑定
     * 4. 颁发 token
     * <p>
     * 必须传递 accessToken 来自 {@link ThirdAuthController#login(String, String, ThirdLoginReq)} 返回的token
     * <p>
     * 首次绑定成功之后, 重新颁发令牌, 即之后所有操作都需要携带该令牌
     */
    @Transactional(rollbackFor = Exception.class)
    public OAuth2TokenResp firstBind(ThirdBindContent content) {
        HttpServletRequest request = ServletUtils.getRequestOfNonNull();
        String terminal = request.getHeader(RequestCons.Headers.TERMINAL);

        // step 校验token令牌, 该令牌来自登录接口返回的token
        OAuth2TokenResp tokenInfo = content.getTokenInfo();
        if (org.apache.commons.lang3.StringUtils.isAnyEmpty(tokenInfo.getOpenId(), tokenInfo.getGrantType())) {
            throw new BizException("不合法的三方授权参数, 请重新发起登录");
        }
        bindVerify(content);

        // 填充三方用户信息, 比如微信小程序可以获取一用户手机号
        fillBindUserInfo(content);

        Assert.notEmpty(content.getAccount(), "账号" + ValidationCons.NOT_EMPTY);

        // step 查找客户端
        RegisteredClient client = clientService.validOAuthClient(tokenInfo.getClientId(), null, null, null, null);
        if (Objects.isNull(client)) {
            throw new BizException("授权客户端不存在, 请重新发起登录");
        }

        AppType appType = client.getAppType();
        GrantTypeEnum grantType = GrantTypeEnum.of(tokenInfo.getGrantType());

        // step 查询三方授权表判断当前第三方用户是否已经绑定了本平台的第三方
        SocialUser socialUser = socialUserService.getByOpenId(grantType, appType, tokenInfo.getOpenId());
        if (Objects.isNull(socialUser)) {
            throw new BizException("三方用户不存在, 请重新发起登录");
        }

        // step 未绑定平台用户, 进行绑定、颁发token流程
        Long userId = socialUser.getUserId();
        if (Objects.isNull(userId)) {
            UserResp user = userService.getUser(content.getAccount(), appType);
            // 由于此时已经确认是本人的手机号/邮箱, 所以可以直接创建新账号, 由于新账号没有密码, 所以不能通过用户名 + 密码方式登录,
            // 只能通过 手机号/邮箱 + 验证码登录, 如果想要使用密码登录, 需要先登录进行设置
            if (Objects.isNull(user)) {
                AuthUserSaveReq userSaveReq = new AuthUserSaveReq();
                userSaveReq.setUsername(StringUtils.defaultIfEmpty(content.getUsername(), "u" + YitIdHelper.nextId()))
                        .setAvatar(content.getAvatar()).setNickName(content.getNickname())
                        .setSex(content.getSexEnum().getCode()).setTerminal(terminal);

                parseAccount(content.getAccount(), userSaveReq);
                userId = userService.saveUser(userSaveReq, appType);
            }
            userId = Optional.ofNullable(user).map(UserResp::getUserId).orElse(userId);
            // 绑定账号
            socialUser.setUserId(userId);
            socialUserService.updateById(socialUser);
        } else {
            throw new BizException("当前账号已被绑定");
        }

        // step 颁发token
        TokenContextBuildReq tokenContextBuildReq = TokenContextBuildReq.builder()
                .userId(userId).loginId(tokenInfo.getLoginId()).client(client)
                .tokenClaims(new HashMap<>()).grantType(grantType).openId(tokenInfo.getOpenId()).build();

        OAuth2TokenContext tokenContext = tokenService.buildTokenContent(tokenContextBuildReq);
        OAuth2TokenResp tokenResp = tokenService.issueToken(tokenContext);
        authHandler.onSuccess(tokenContext.getRegisteredClient(), tokenContext.getAuthUser());
        return tokenResp;
    }

    private void parseAccount(String account, AuthUserSaveReq req) {
        String email = "";
        String phone = "";
        // 判断账号类型
        if (RegexUtils.checkEmail(account)) {
            email = account;
        } else if (RegexUtils.checkPhone(account)) {
            phone = account;
        } else {
            throw new BizException("只支持手机号/邮箱绑定");
        }
        req.setEmail(email);
        req.setPhone(phone);
    }

    public abstract GrantTypeEnum grantType();

    /**
     * 第三方登录
     *
     * @return 返回登录成功后的用户信息
     */
    protected ThirdAuthUser doLogin(ThirdLoginReq req) {
        throw new BizException(AuthErrorEnum.NO_IMPL_GRANT_TYPE, grantType().getCode());
    }

    /**
     * 返回带{@code state}参数的授权url，授权回调时会带上这个{@code state}
     * <p>
     * state 验证授权流程的参数，可以防止csrf
     *
     * @return 返回授权地址
     */
    protected ThirdAuthResp doAuthorize(AuthUrlGetReq req) {
        throw new BizException(AuthErrorEnum.NO_IMPL_GRANT_TYPE, grantType().getCode());
    }

    /**
     * 获取第三方重定向url
     *
     * @param redirectUri 配置的第三方回调url
     * @param backUrl     当前系统的前端页面登录成功返回的地址
     * @param client      客户端
     */
    protected String getThirdRedirectUri(String redirectUri, String backUrl, RegisteredClient client) {
        // 这里不需要将 clientId 和 grantType参数拼接到路径上, 但是要求, 前端在调用后端接口获取授权地址且
        // 在跳转前要将clientId和grantType存储在本地, 当第三方回调到前端页面时候, 前端再从本地缓存取并调用且传递给login接口
        return StringUtils.isEmpty(backUrl)
                ? redirectUri
                : String.format("%s?backUrl=%s", redirectUri, backUrl);
    }

    /**
     * 获取登录二维码
     */
    public LoginQrcodeResp getLoginQrcode(LoginQrcodeGetReq req) {
        String qrCodeId = String.valueOf(YitIdHelper.nextId());
        LoginQrcodeResp resp = new LoginQrcodeResp();

        JSONObject params = new JSONObject();
        params.put(ScanScene.QR_CODE_ID, qrCodeId);
        ScanScene scanScene = ScanScene.builder()
                .isRefresh(false).params(params).isTmpTicket(true).expireSeconds(60)
                .sceneCode(QrCodeSceneEnum.SCAN_LOGIN.getCode()).build();
        String qrCodeUrl = this.doGetLoginQrcodeUrl(scanScene);
        ;

        resp.setQrcodeId(qrCodeId);
        resp.setQrCodeUrl(qrCodeUrl);

        // 将数据存入缓存中
        CacheKeyEnum keyEnum = CacheKeyEnum.LOGIN_QR_CODE;
        String cacheKey = keyEnum.formatKey(qrCodeId);
        LoginQrcodeInfo cacheValue = new LoginQrcodeInfo();
        cacheValue.setIsScan(false).setClientId(req.getClientId()).setGrantType(req.getGrantType());

        this.redisTemplate.opsForValue().set(cacheKey, cacheValue, keyEnum.getExpire(), keyEnum.getUnit());

        return resp;
    }

    protected String doGetLoginQrcodeUrl(ScanScene scanScene) {
        throw new BizException(AuthErrorEnum.NO_IMPL_GRANT_TYPE, grantType().getCode());
    }

    /**
     * 获取登录二维码的token
     */
    public LoginQrcodeTokenResp getQrcodeToken(RegisteredClient client, LoginQrcodeInfo req) {
        // step 构建授权用户, 目前只设置openId
        ThirdAuthUser authUser = new ThirdAuthUser();

        authUser.setOpenId(req.getOpenId()).setSource(grantType().getCode())
                .setLoginId(String.valueOf(YitIdHelper.nextId())).setUsername("user_" + YitIdHelper.nextId())
                .setNickname("用户_" + RandomUtil.randomInt(100000, 900000)).setSex(SexEnum.UNKNOWN.getCode());

        // step: 执行绑定用户逻辑
        autoBindUser(client, authUser);

        // step: 办法token, 无论用户是否已经绑定平台用户都要颁发token, 假如用户没有绑定平台用户且没有开启自动绑定,
        // 那么前端要引导用户进行绑定, 在绑定已有账号或者创建新账号的时候都要携带该令牌, 即接口必须授权才能访问
        TokenContextBuildReq tokenContextBuildReq = TokenContextBuildReq.builder()
                .loginAccount(req.getOpenId()).userId(authUser.getUserId())
                .tokenClaims(authUser.getTokenClaims()).loginAccount(authUser.getOpenId())
                .loginId(authUser.getLoginId()).client(client).grantType(grantType()).openId(authUser.getOpenId()).build();
        OAuth2TokenResp token = tokenService.issueToken(tokenService.buildTokenContent(tokenContextBuildReq));
        authUser.setAuthToken(token);

        LoginQrcodeTokenResp resp = thirdAssembly.toLoginQrcodeTokenResp(authUser);
        resp.setIsScan(req.getIsScan());
        return resp;
    }
}
